﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.CodeDom.Compiler;
using System.Runtime.Serialization;

namespace Kyoh.Lib.Plugin
{
    /// <summary>プラグインを管理します。</summary>
    /// <typeparam name="T">管理対象のプラグインの基本となるインターフェースの方を指定します。</typeparam>
    public class PluginManager<T> : BindingList<T>, IDisposable, ITypedList, ISerializable, IComparer<IPlugin>
        where T : class, IPlugin
    {
        private List<Guid> _order;
        public PluginManager() { }

        protected PluginManager(SerializationInfo info, StreamingContext context)
        {

        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }

        public int Compare(IPlugin x, IPlugin y)
        {
            throw new NotImplementedException();
        }

        private PluginLoader<T> loader = null;
        private PluginLoader<T> Loader
        {
            get
            {
                if (loader == null)
                    loader = new PluginLoader<T>();
                return loader;
            }
        }

        public CompilerParameters CompilerParameters { get { return Loader.CompilerParameters; } }

        /// <summary>指定されたファイルの列挙から、プラグインのインスタンスをロードしてコレクションに追加します。</summary>
        /// <param name="files">DLL、C#、IronPythonのいずれかのパスの列挙。</param>
        public void Load(IEnumerable<string> files)
        {
            AddRange(SelectMany(files, Loader.Load).Where(t => t != null));
        }

        private static IEnumerable<T> SelectMany(IEnumerable<string> source, Func<string, IEnumerable<T>> selector)
        {
            Exception exceptions = null;
            IEnumerable<T> result;
            foreach (var file in source)
            {
                result = null;
                try
                {
                    result = selector(file);
                }
                catch (Exception exc) { exceptions = GetAggregateException(exceptions, exc); }
                if (result == null)
                    continue;
                IEnumerator<T> enumerator = null;
                try
                {
                    enumerator = result.GetEnumerator();
                }
                catch (Exception exc) { exceptions = GetAggregateException(exceptions, exc); }

                if (enumerator == null)
                    continue;
                bool next = false;
                do
                {
                    next = false;
                    try { next = enumerator.MoveNext(); }
                    catch (Exception exc) { exceptions = GetAggregateException(exceptions, exc); }
                    if (!next)
                        break;
                    yield return enumerator.Current;
                } while (next);
            }
            if (exceptions != null)
                throw exceptions;
        }

        private static Exception GetAggregateException(Exception exc, Exception newException)
        {
            if (exc == null)
                return newException;
            if (newException == null)
                return exc;
            return new AggregateException(exc, newException).Flatten();
        }

        /// <summary>指定されたアセンブリから、プラグインのインスタンスをロードしてコレクションに追加します。</summary>
        /// <param name="assembly">生成元のアセンブリ。</param>
        public void Load(Assembly assembly)
        {
            AddRange(PluginLoader<T>.CreateInstances(assembly, false));
        }
        
        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        ~PluginManager()
        {
            Dispose(false);
        }

        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
        protected void Dispose(bool disposing)
        {
            if (disposing)
                Loader.Dispose();
        }

        /// <summary>指定したコレクションの要素を <see cref="System.Collections.Generic.List{T}"/> の末尾に追加します。</summary>
        /// <param name="collection">
        /// <see cref="System.Collections.Generic.List{T}"/> の末尾に要素が追加されるコレクション。
        /// コレクション自体を <c>null</c> にすることはできません。また、コレクションに格納する要素も <c>null</c> にすることはできません。
        /// </param>
        /// <exception cref="ArgumentNullException"><paramref name="collection"/> が <c>null</c> です。</exception>
        public void AddRange(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");
            foreach (var item in collection)
                Add(item);
        }

        /// <summary>すべてのプラグインをアンロードします。</summary>
        protected override void ClearItems()
        {
            Loader.Clear();
            base.ClearItems();
        }

        /// <summary>指定されたインデックスのプラグインをアンロードします。</summary>
        /// <param name="index">アンロード対象のプラグインを示すインデックス。</param>
        protected override void RemoveItem(int index)
        {
            Loader.Unload(this[index]);
            base.RemoveItem(index);
        }

        /// <summary>指定されたインデックスにプラグインを追加します。</summary>
        protected override void InsertItem(int index, T item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            base.InsertItem(index, item);
        }

        /// <summary>このオブジェクトではサポートされません。</summary>
        protected override void SetItem(int index, T item)
        {
            throw new NotSupportedException();
        }

        /// <summary>項目の <see cref="PropertyDescriptorCollection"/> を参照します。</summary>
        public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
        {
            var props = new PropertyDescriptorCollection(new PropertyDescriptor[] { });
            foreach (var prop in GetInterfaceProperties(typeof(T)))
                if (!props.Contains(prop))
                    props.Add(prop);
            return props;
        }

        private static IEnumerable<PropertyDescriptor> GetInterfaceProperties(Type t)
        {
            if (t.IsInterface)
            {
                foreach (var baseInterface in t.GetInterfaces())
                    foreach (var prop in GetInterfaceProperties(baseInterface))
                        yield return prop;
            }
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(t))
                yield return prop;
        }

        public string GetListName(PropertyDescriptor[] listAccessors)
        {
            return "PluginManager";
        }
    }
}
